/**
 * @license
 * Copyright Alibaba.com All Rights Reserved.
 *
 * Use of this source code is governed by an MIT-style license that can be
 * found in the LICENSE file at https://github.com/NG-ZORRO/ng-zorro-antd/blob/master/LICENSE
 */

import {
  forwardRef,
  ChangeDetectionStrategy,
  ChangeDetectorRef,
  Component,
  ContentChild,
  EventEmitter,
  Host,
  Input,
  OnChanges,
  OnDestroy,
  OnInit,
  Optional,
  Output,
  SimpleChange,
  SkipSelf,
  TemplateRef
} from '@angular/core';
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
import { Observable, ReplaySubject, Subject } from 'rxjs';
import { takeUntil } from 'rxjs/operators';

import {
  isNotNil,
  toBoolean,
  warnDeprecation,
  InputBoolean,
  ApesFormatBeforeDropEvent,
  ApesFormatEmitEvent,
  ApesNoAnimationDirective,
  ApesTreeBase,
  ApesTreeBaseService,
  ApesTreeHigherOrderServiceToken,
  ApesTreeNode, WithConfig
} from '@shared/apes/core';

import { ApesTreeService } from './apes-tree.service';

export function ApesTreeServiceFactory(
  higherOrderService: ApesTreeBaseService,
  treeService: ApesTreeService
): ApesTreeBaseService {
  return higherOrderService ? higherOrderService : treeService;
}

@Component({
  selector: 'apes-tree',
  exportAs: 'apesTree',
  templateUrl: './apes-tree.component.html',
  changeDetection: ChangeDetectionStrategy.OnPush,
  providers: [
    ApesTreeService,
    {
      provide: ApesTreeBaseService,
      useFactory: ApesTreeServiceFactory,
      deps: [[new SkipSelf(), new Optional(), ApesTreeHigherOrderServiceToken], ApesTreeService]
    },
    {
      provide: NG_VALUE_ACCESSOR,
      useExisting: forwardRef(() => ApesTreeComponent),
      multi: true
    }
  ]
})
export class ApesTreeComponent extends ApesTreeBase implements OnInit, OnDestroy, ControlValueAccessor, OnChanges {
  @Input() @InputBoolean() apesShowIcon = false;
  @Input() @InputBoolean() apesShowExpand = true;
  @Input() @InputBoolean() apesShowLine = false;
  @Input() apesExpandedIcon: TemplateRef<{ $implicit: ApesTreeNode }>;
  @Input() @InputBoolean() apesCheckable = false;
  @Input() @InputBoolean() apesAsyncData = false;
  @Input() @InputBoolean() apesDraggable = false;

  @Input() @InputBoolean() apesHideUnMatched = false;
  @Input() @InputBoolean() apesSelectMode = false;
  @Input() @InputBoolean() apesCheckStrictly = false;
  @Input() @InputBoolean() apesBlockNode = false;

  @Input() @ContentChild('ApesTreeTemplate', { static: true }) apesTreeTemplate: TemplateRef<{ $implicit: ApesTreeNode }>;

  @Input() @InputBoolean() apesExpandAll = false;

  /**
   * @deprecated 9.0.0 use `apesExpandAll` instead.
   */
  @Input()
  @InputBoolean()
  set apesDefaultExpandAll(value: boolean) {
    warnDeprecation(`'apesDefaultExpandAll' would be removed in 9.0.0. Please use 'apesExpandAll' instead.`);
    this.apesExpandAll = value;
    this._apesDefaultExpandAll = value;
  }

  get apesDefaultExpandAll(): boolean {
    return this._apesDefaultExpandAll;
  }

  private _apesDefaultExpandAll = false;

  @Input() apesBeforeDrop: (confirm: ApesFormatBeforeDropEvent) => Observable<boolean>;

  @Input()
  @InputBoolean()
  set apesMultiple(value: boolean) {
    this._apesMultiple = toBoolean(value);
    this.apesTreeService.isMultiple = toBoolean(value);
  }

  get apesMultiple(): boolean {
    return this._apesMultiple;
  }

  @Input()
  // tslint:disable-next-line:no-any
  set apesData(value: any[]) {
    this.initApesData(value);
  }

  /**
   * @deprecated 9.0.0 - use `apesExpandedKeys` instead.
   */
  @Input()
  set apesDefaultExpandedKeys(value: string[]) {
    warnDeprecation(`'apesDefaultExpandedKeys' would be removed in 9.0.0. Please use 'apesExpandedKeys' instead.`);
    this.apesDefaultSubject.next({ type: 'apesExpandedKeys', keys: value });
  }

  /**
   * @deprecated 9.0.0 - use `apesSelectedKeys` instead.
   */
  @Input()
  set apesDefaultSelectedKeys(value: string[]) {
    warnDeprecation(`'apesDefaultSelectedKeys' would be removed in 9.0.0. Please use 'apesSelectedKeys' instead.`);
    this.apesDefaultSubject.next({ type: 'apesSelectedKeys', keys: value });
  }

  /**
   * @deprecated 9.0.0 - use `apesCheckedKeys` instead.
   */
  @Input()
  set apesDefaultCheckedKeys(value: string[]) {
    warnDeprecation(`'apesDefaultCheckedKeys' would be removed in 9.0.0. Please use 'apesCheckedKeys' instead.`);
    this.apesDefaultSubject.next({ type: 'apesCheckedKeys', keys: value });
  }

  @Input()
  set apesExpandedKeys(value: string[]) {
    this.apesDefaultSubject.next({ type: 'apesExpandedKeys', keys: value });
  }

  @Input()
  set apesSelectedKeys(value: string[]) {
    this.apesDefaultSubject.next({ type: 'apesSelectedKeys', keys: value });
  }

  @Input()
  set apesCheckedKeys(value: string[]) {
    this.apesDefaultSubject.next({ type: 'apesCheckedKeys', keys: value });
  }

  @Input()
  set apesSearchValue(value: string) {
    this._searchValue = value;
    this.apesTreeService.searchExpand(value);
    if (isNotNil(value)) {
      this.apesSearchValueChange.emit(this.apesTreeService.formatEvent('search', null, null));
      /**
       * @deprecated 9.0.0 - use `apesOnSearchNode` instead.
       * Hide warning, need remove next version
       */
      this.apesOnSearchNode.emit(this.apesTreeService.formatEvent('search', null, null));
    }
  }

  get apesSearchValue(): string {
    return this._searchValue;
  }

  /**
   * To render nodes if root is changed.
   */
  get apesNodes(): ApesTreeNode[] {
    return this.apesTreeService.rootNodes;
  }

  @Output() readonly apesExpandedKeysChange: EventEmitter<string[]> = new EventEmitter<string[]>();
  @Output() readonly apesSelectedKeysChange: EventEmitter<string[]> = new EventEmitter<string[]>();
  @Output() readonly apesCheckedKeysChange: EventEmitter<string[]> = new EventEmitter<string[]>();

  @Output() readonly apesSearchValueChange = new EventEmitter<ApesFormatEmitEvent>();

  /**
   * @deprecated use `apesSearchValueChange` instead.
   */
  @Output() readonly apesOnSearchNode = new EventEmitter<ApesFormatEmitEvent>();

  @Output() readonly apesClick = new EventEmitter<ApesFormatEmitEvent>();
  @Output() readonly apesDblClick = new EventEmitter<ApesFormatEmitEvent>();
  @Output() readonly apesContextMenu = new EventEmitter<ApesFormatEmitEvent>();
  @Output() readonly apesCheckBoxChange = new EventEmitter<ApesFormatEmitEvent>();
  @Output() readonly apesExpandChange = new EventEmitter<ApesFormatEmitEvent>();

  @Output() readonly apesOnDragStart = new EventEmitter<ApesFormatEmitEvent>();
  @Output() readonly apesOnDragEnter = new EventEmitter<ApesFormatEmitEvent>();
  @Output() readonly apesOnDragOver = new EventEmitter<ApesFormatEmitEvent>();
  @Output() readonly apesOnDragLeave = new EventEmitter<ApesFormatEmitEvent>();
  @Output() readonly apesOnDrop = new EventEmitter<ApesFormatEmitEvent>();
  @Output() readonly apesOnDragEnd = new EventEmitter<ApesFormatEmitEvent>();

  _searchValue: string;
  _apesMultiple: boolean = false;
  apesDefaultSubject = new ReplaySubject<{ type: string; keys: string[] }>(6);
  destroy$ = new Subject();
  prefixCls = 'apes-tree';
  classMap = {};

  onChange: (value: ApesTreeNode[]) => void = () => null;
  onTouched: () => void = () => null;

  setClassMap(): void {
    this.classMap = {
      [this.prefixCls]: true,
      [this.prefixCls + '-show-line']: this.apesShowLine,
      [`${this.prefixCls}-icon-hide`]: !this.apesShowIcon,
      [`${this.prefixCls}-block-node`]: this.apesBlockNode,
      ['draggable-tree']: this.apesDraggable,
      ['apes-select-tree']: this.apesSelectMode
    };
  }

  writeValue(value: ApesTreeNode[]): void {
    this.initApesData(value);
  }

  registerOnChange(fn: (_: ApesTreeNode[]) => void): void {
    this.onChange = fn;
  }

  registerOnTouched(fn: () => void): void {
    this.onTouched = fn;
  }

  // tslint:disable-next-line:no-any
  initApesData(value: any[]): void {
    if (Array.isArray(value)) {
      this.apesTreeService.isCheckStrictly = this.apesCheckStrictly;
      this.apesTreeService.isMultiple = this.apesMultiple;
      this.apesTreeService.initTree(this.coerceTreeNodes(value));
    }
  }

  constructor(
    ApesTreeService: ApesTreeBaseService,
    private cdr: ChangeDetectorRef,
    @Host() @Optional() public noAnimation?: ApesNoAnimationDirective
  ) {
    super(ApesTreeService);
  }

  ngOnInit(): void {
    this.setClassMap();
    this.apesDefaultSubject.pipe(takeUntil(this.destroy$)).subscribe((data: { type: string; keys: string[] }) => {
      if (!data || !data.keys) {
        return;
      }
      switch (data.type) {
        case 'apesExpandedKeys':
          this.apesTreeService.calcExpandedKeys(data.keys, this.apesNodes);
          this.apesExpandedKeysChange.emit(data.keys);
          break;
        case 'apesSelectedKeys':
          this.apesTreeService.calcSelectedKeys(data.keys, this.apesNodes, this.apesMultiple);
          this.apesSelectedKeysChange.emit(data.keys);
          break;
        case 'apesCheckedKeys':
          this.apesTreeService.calcCheckedKeys(data.keys, this.apesNodes, this.apesCheckStrictly);
          this.apesCheckedKeysChange.emit(data.keys);
          break;
      }
      this.cdr.markForCheck();
    });
    this.apesTreeService
      .eventTriggerChanged()
      .pipe(takeUntil(this.destroy$))
      .subscribe(data => {
        switch (data.eventName) {
          case 'expand':
            this.apesExpandChange.emit(data);
            break;
          case 'click':
            this.apesClick.emit(data);
            break;
          case 'check':
            this.apesCheckBoxChange.emit(data);
            break;
          case 'dblclick':
            this.apesDblClick.emit(data);
            break;
          case 'contextmenu':
            this.apesContextMenu.emit(data);
            break;
          // drag drop
          case 'dragstart':
            this.apesOnDragStart.emit(data);
            break;
          case 'dragenter':
            this.apesOnDragEnter.emit(data);
            break;
          case 'dragover':
            this.apesOnDragOver.emit(data);
            break;
          case 'dragleave':
            this.apesOnDragLeave.emit(data);
            break;
          case 'drop':
            this.apesOnDrop.emit(data);
            break;
          case 'dragend':
            this.apesOnDragEnd.emit(data);
            break;
        }
      });
  }

  ngOnChanges(changes: { [propertyName: string]: SimpleChange }): void {
    if (changes.apesCheckStrictly) {
      this.apesTreeService.isCheckStrictly = toBoolean(changes.apesCheckStrictly.currentValue);
    }
    if (changes.apesMultiple) {
      this.apesTreeService.isMultiple = toBoolean(changes.apesMultiple.currentValue);
    }
  }

  ngOnDestroy(): void {
    this.destroy$.next();
    this.destroy$.complete();
  }
}
